// Copyright (C) 2009 Jesse Jones
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using MmmDoc;
using System;
using System.Collections.Generic;
using System.IO;
using System.Web;

namespace MmmDoc.Html
{
	/// <summary>Used to write HTML elements and attributes to a file.</summary>
	internal sealed class Writer : IDisposable
	{
		/// <param name = "file">Path to the file which will be generated. If it already
		/// exists it will be replaced.</param>
		/// <remarks>This will add the DOCTYPE and HTML tags.</remarks>
		public Writer(string file)
		{
			Contract.Requires(!string.IsNullOrEmpty(file), "file is null or empty");
			
			// The behavior of StreamWriter isn't documented very well if the file already
			// exists so we'll be safe and delete it if it does exist so we don't wind up with
			// old bits at the end.
			if (File.Exists(file))
				File.Delete(file);
				
			m_writer = new StreamWriter(file);	// defaults to utf-8
		}
		
		/// <summary>Flushes the data to disk.</summary>
		public void Dispose()
		{
			Contract.Requires(m_tags.Count == 0, "didn't end all tags");
			
			if (!m_disposed)
			{
				m_writer.Dispose();
				m_disposed = true;
			}
		}
		
		public void WriteLine()
		{
			if (m_disposed)
				throw new ObjectDisposedException(GetType().Name);
			
			DoPadLineStart();
			m_writer.WriteLine();
			m_needsIndent = true;
		}
		
		public void UnencodedWrite(string text)
		{
			if (m_disposed)
				throw new ObjectDisposedException(GetType().Name);
			
			DoPadLineStart();
			m_writer.Write(text);
		}
		
		public void Write(string text)
		{
			if (m_disposed)
				throw new ObjectDisposedException(GetType().Name);
			
			DoPadLineStart();
			m_writer.Write(HttpUtility.HtmlEncode(text));
		}
		
		public void WriteLine(string text)
		{
			if (m_disposed)
				throw new ObjectDisposedException(GetType().Name);
			
			Write(text);
			m_writer.WriteLine();
			m_needsIndent = true;
		}
		
		/// <summary>Writes a start tag with optional attributes.</summary>
		/// <param name = "name">The name of the tag. Note that this should not include the angle brackets.</param>
		/// <param name = "attributes">Pairs of attribute names and the attribute value.</param>
		public void WriteStartTag(string name, params object[] attributes)
		{
			Contract.Requires(!string.IsNullOrEmpty(name), "name is empty or null");
			Contract.Requires(attributes != null, "attributes is null");
			Contract.Requires(attributes.Length % 2 == 0, "attributes are not paired");
			if (m_disposed)
				throw new ObjectDisposedException(GetType().Name);
			
			DoPadLineStart();
			m_writer.Write('<');
			m_writer.Write(name);
			DoWriteAttributes(attributes);
			m_writer.Write('>');
			
			m_tags.Push(name);
		}
		
		/// <remarks>Indents the line after writing.</remarks>
		public void WriteStartTagLine(string name, params object[] attributes)
		{
//	Console.WriteLine("{0}<{1}>", new string(' ', 4*m_indent), name);
			if (m_disposed)
				throw new ObjectDisposedException(GetType().Name);
			
			WriteStartTag(name, attributes);
			m_writer.WriteLine();
			
			++m_indent;
			m_needsIndent = true;
		}
		
		/// <summary>Closes a WriteStartTag.</summary>
		/// <param name = "name">The name of the tag. This should match the name used with the WriteStartTag.</param>
		public void WriteEndTag(string name)
		{
//	Console.WriteLine("{0}</{1}>", new string(' ', 4*m_indent), name);
			Contract.Requires(!string.IsNullOrEmpty(name), "name is null or empty");
			Contract.Requires(m_tags.Count > 0, "no open tag");
			Contract.Requires(m_tags.Peek() == name, string.Format("last open tag is {0} but closing with {1}", m_tags.Peek(), name));
			if (m_disposed)
				throw new ObjectDisposedException(GetType().Name);
				
			m_tags.Pop();
			
			DoPadLineStart();
			m_writer.Write("</");
			m_writer.Write(name);
			m_writer.Write('>');
		}
		
		/// <remarks>Outdents the line before writing.</remarks>
		public void WriteEndTagLine(string name)
		{
			Contract.Requires(m_indent > 0, "indent is already zero");
			
			if (m_disposed)
				throw new ObjectDisposedException(GetType().Name);
			
			--m_indent;
			WriteEndTag(name);
			m_writer.WriteLine();
			
			m_needsIndent = true;
		}
		
		/// <summary>Writes an element with optional text content and attributes.</summary>
		/// <param name = "name">The name of the tag. Note that this should not include the angle brackets.</param>
		/// <param name = "text">May be null.</param>
		/// <param name = "attributes">Pairs of attribute names and the attribute value.</param>
		public void WriteElement(string name, string text, params object[] attributes)
		{
			Contract.Requires(!string.IsNullOrEmpty(name), "name is empty or null");
			Contract.Requires(attributes != null, "attributes is null");
			Contract.Requires(attributes.Length % 2 == 0, "attributes are not paired");
			if (m_disposed)
				throw new ObjectDisposedException(GetType().Name);
			
			DoPadLineStart();
			m_writer.Write('<');
			m_writer.Write(name);
			DoWriteAttributes(attributes);
			if (string.IsNullOrEmpty(text))
			{
				m_writer.Write(">");
			}
			else
			{
				m_writer.Write('>');
				m_writer.Write(HttpUtility.HtmlEncode(text));
				m_writer.Write("</");
				m_writer.Write(name);
				m_writer.Write('>');
			}
		}
		
		public void WriteElementLine(string name, string text, params object[] attributes)
		{
			if (m_disposed)
				throw new ObjectDisposedException(GetType().Name);
			
			WriteElement(name, text, attributes);
			m_writer.WriteLine();
			m_needsIndent = true;
		}
		
		#region Private Methods
		private void DoPadLineStart()
		{
			if (m_needsIndent)
			{
				m_writer.Write(new string(' ', 4*m_indent));
				m_needsIndent = false;
			}
		}
		
		private void DoWriteAttributes(object[] attributes)
		{
			for (int i = 0; i < attributes.Length; i += 2)
			{
				m_writer.Write(' ');
				m_writer.Write(attributes[i].ToString());
				m_writer.Write(" = \"");
				m_writer.Write(HttpUtility.HtmlAttributeEncode(attributes[i + 1].ToString()));
				m_writer.Write("\"");
			}
		}
		#endregion
		
		#region Fields
		private TextWriter m_writer;
		private bool m_needsIndent;
		private int m_indent;
		private bool m_disposed;
		private Stack<string> m_tags = new Stack<string>();
		#endregion
	}
}
